import * as fs from "node:fs"
import * as path from "node:path"
import { Pixel, TRANSPARENT } from "./hp3-img.js";
import { loadPng, savePng } from "./hp3-png.js";
import { formatXml, KHashTable, KXMLDocument, KXMLTag, numcmp, numhash, strcmp, strhash } from "@coldcloude/kai2";

export type SpriteFrameInfo = {
    id: string,
    positionX: number,
    positionY: number,
    width: number,
    height: number
};

export type AtlasFrameInfo = {
    positionX: number,
    positionY: number,
    frame: SpriteFrameInfo
};

export type AtlasInfo = {
    width: number,
    height: number,
    frames: AtlasFrameInfo[]
}

const BATCH_PIXEL = 128;

export function calculateAtlas(frames:SpriteFrameInfo[],margin?:number,paddings?:{up:number,down:number,left:number,right:number}):AtlasInfo{
    margin = margin||0;
    paddings = paddings||{
        up: 0,
        down: 0,
        left: 0,
        right: 0
    };
    let w = 1;
    let lastH = 0;
    let lastComponents:AtlasFrameInfo[] = [];
    let finished = false;
    while(!finished){
        let success = true;
        let x = margin;
        let y = margin;
        let lastY = margin;
        const components:AtlasFrameInfo[] = [];
        for(const f of frames){
            const minWidth = f.width+margin*2+paddings.left+paddings.right;
            if(minWidth>w*BATCH_PIXEL){
                success = false;
                w = Math.max(w+1,Math.ceil(minWidth/BATCH_PIXEL)|0);
                break;
            }
            const dw = paddings.left+f.width+paddings.right+margin;
            const dh = paddings.up+f.height+paddings.down+margin;
            if(x+dw<=w*BATCH_PIXEL){
                //same line
                components.push({
                    positionX: x+paddings.left,
                    positionY: lastY+paddings.up,
                    frame: f
                });
                x += dw;
                y = Math.max(y,dh);
            }
            else{
                //new line
                lastY += y;
                components.push({
                    positionX: margin+paddings.left,
                    positionY: lastY+paddings.up,
                    frame: f
                });
                x = margin+dw;
                y = dh;
            }
        }
        if(success){
            const h = Math.ceil((lastY+y)/BATCH_PIXEL)|0
            if(w>h){
                finished = true;
                if(lastH===0){
                    lastH = h;
                    lastComponents = components;
                }
                else{
                    w--;
                }
            }
            else{
                lastH = h;
                lastComponents = components;
                w++;
            }
        }
    }
    return {
        width: w*BATCH_PIXEL,
        height: lastH*BATCH_PIXEL,
        frames: lastComponents
    }
}
export function saveAtlasPng(atlas:AtlasInfo,frameDir:string,outputFile:string){
    const pixels:Pixel[] = [];
    const width = atlas.width;
    const height = atlas.height;
    for(let y=0; y<height; y++){
        for(let x=0; x<width; x++){
            pixels[y*width+x] = TRANSPARENT;
        }
    }
    for(const component of atlas.frames){
        const id = component.frame.id;
        const w = component.frame.width;
        const h = component.frame.height;
        const dx = component.positionX;
        const dy = component.positionY;
        const part = id.substring(0,3);
        const file = path.join(frameDir,part,id+".png");
        if(fs.existsSync(file)){
            const img = loadPng(file);
            for(let y=0; y<h; y++){
                for(let x=0; x<w; x++){
                    const src = y*w+x;
                    const dst = (y+dy)*width+x+dx;
                    if(src<img.pixels.length){
                        pixels[dst] = img.pixels[src];
                    }
                }
            }
        }
    }
    savePng(width,height,pixels,outputFile);
}

export function saveAtlasPlist(atlas:AtlasInfo,atlasPngFileName:string,outputFile:string){
    const frameTags:KXMLTag[] = [];
    for(const component of atlas.frames){
        const id = component.frame.id;
        const w = component.frame.width;
        const h = component.frame.height;
        const dx = component.positionX;
        const dy = component.positionY;
        frameTags.push({name:"key",type:"2-side",children:[id+".png"]});
        frameTags.push({name:"dict",type:"2-side",children:[
            {name:"key",type:"2-side",children:["spriteOffset"]},
            {name:"string",type:"2-side",children:["{0,0}"]},
            {name:"key",type:"2-side",children:["spriteSize"]},
            {name:"string",type:"2-side",children:["{"+w+","+h+"}"]},
            {name:"key",type:"2-side",children:["spriteSourceSize"]},
            {name:"string",type:"2-side",children:["{"+w+","+h+"}"]},
            {name:"key",type:"2-side",children:["textureRect"]},
            {name:"string",type:"2-side",children:["{{"+dx+","+dy+"},{"+w+","+h+"}}"]},
            {name:"key",type:"2-side",children:["textureRotated"]},
            {name:"false",type:"single"},
        ]});
    }
    const plist:KXMLDocument = {
        header: {version:"1.0",encoding:"UTF-8"},
        dtd: {rootTagName:"plist",dtdType:"PUBLIC",dtdName:"-//Apple Computer//DTD PLIST 1.0//EN",dtdUrl:"http://www.apple.com/DTDs/PropertyList-1.0.dtd"},
        rootTag: {name:"plist",type:"2-side",attributes:[["version","1.0"]],children:[
            {name:"dict",type:"2-side",children:[
                {name:"key",type:"2-side",children:["frames"]},
                {name:"dict",type:"2-side",children:frameTags},
                {name:"key",type:"2-side",children:["metadata"]},
                {name:"dict",type:"2-side",children:[
                    {name:"key",type:"2-side",children:["format"]},
                    {name:"integer",type:"2-side",children:["3"]},
                    {name:"key",type:"2-side",children:["realTextureFileName"]},
                    {name:"string",type:"2-side",children:[atlasPngFileName]},
                    {name:"key",type:"2-side",children:["size"]},
                    {name:"string",type:"2-side",children:["{"+atlas.width+","+atlas.height+"}"]},
                    {name:"key",type:"2-side",children:["textureFileName"]},
                    {name:"string",type:"2-side",children:[atlasPngFileName]},
                ]}
            ]}
        ]}
    };
    fs.writeFileSync(outputFile,formatXml(plist,"  "),"utf8");
}

export type SpriteAnimeInfo = {
    direction: number,
    action: number,
    duration: number,
    frameIds: string[]
}

export type SpriteAnimeCollectionInfo = {
    id: string,
    animes: SpriteAnimeInfo[],
    frames: SpriteFrameInfo[]
}

export function packSpriteAnimeCollection(aniColle:SpriteAnimeCollectionInfo,frameDir:string,outputDir:string,outputInfo:boolean,outputAtlas:boolean){
    if(!fs.existsSync(outputDir)){
        fs.mkdirSync(outputDir,{recursive:true});
    }
    //output info
    if(outputInfo){
        fs.writeFileSync(path.join(outputDir,aniColle.id+".json"),JSON.stringify(aniColle));
    }
    //output atlas
    if(outputAtlas){
        const atlas = calculateAtlas(aniColle.frames);
        const atlasPngFileName = aniColle.id+".png";
        const atlasPlistFileName = aniColle.id+".plist";
        saveAtlasPng(atlas,frameDir,path.join(outputDir,atlasPngFileName));
        saveAtlasPlist(atlas,atlasPngFileName,path.join(outputDir,atlasPlistFileName));
    }
}

export type MapTileInfo = SpriteFrameInfo&{
    mapId: number
    gridWidth: number,
    gridHeight: number,
}

export type MapLayerInfo = {
    name: string,
    type: "tile"|"object"|"tag"
    grids: number[],
}

export type MapInfo = {
    name: string,
    tileWidth: number,
    tileHeight: number,
    gridWidth: number,
    gridHeight: number,
    layers: MapLayerInfo[],
    tiles: MapTileInfo[],
    objects: MapTileInfo[]
}

function buildTileTag(index:number,frame:AtlasFrameInfo,src:string,srcWidth:number,srcHeight:number):KXMLTag{
    return {
        name: "tile",
        type: "2-side",
        attributes: [
            ["id",index.toString()],
            ["x",frame.positionX.toString()],
            ["y",frame.positionY.toString()],
            ["width",frame.frame.width.toString()],
            ["height",frame.frame.height.toString()]
        ],
        children: [{
            name: "image",
            type: "single",
            attributes: [
                ["source",src],
                ["width",srcWidth.toString()],
                ["height",srcHeight.toString()]
            ]
        }]
    };
}

export function packMapInfo(map:MapInfo,frameDir:string,outputDir:string,outputInfo:boolean,outputAtlas:boolean,outputTileMap:boolean,tilePaddings?:{up:number,down:number,left:number,right:number}){
    if(!fs.existsSync(outputDir)){
        fs.mkdirSync(outputDir,{recursive:true});
    }
    //output info
    if(outputInfo){
        fs.writeFileSync(path.join(outputDir,map.name+"_info.json"),JSON.stringify(map));
    }
    //build atlas
    if(outputAtlas||outputTileMap){
        const tileAtlas = calculateAtlas(map.tiles,0,tilePaddings);
        const tileAtlasPngFileName = map.name+"_tiles.png";
        const tileAtlasPlistFileName = map.name+"_tiles.plist";
        const objectAtlas = calculateAtlas(map.objects,1);
        const objectAtlasPngFileName = map.name+"_objects.png";
        const objectAtlasPlistFileName = map.name+"_objects.plist";
        if(outputAtlas){
            saveAtlasPng(tileAtlas,frameDir,path.join(outputDir,tileAtlasPngFileName));
            saveAtlasPlist(tileAtlas,tileAtlasPngFileName,path.join(outputDir,tileAtlasPlistFileName));
            saveAtlasPng(objectAtlas,frameDir,path.join(outputDir,objectAtlasPngFileName));
            saveAtlasPlist(objectAtlas,objectAtlasPngFileName,path.join(outputDir,objectAtlasPlistFileName));
        }
        if(outputTileMap){
            const mapId2index = new KHashTable<number,number>(numcmp,numhash);
            const index2tileFrameId = new KHashTable<number,string>(numcmp,numhash);
            const index2objectFrameId = new KHashTable<number,string>(numcmp,numhash);
            let index = 1;
            for(const tile of map.tiles){
                mapId2index.set(tile.mapId,index);
                index2tileFrameId.set(index,tile.id);
                index++;
            }
            for(const tile of map.objects){
                mapId2index.set(tile.mapId,index);
                index2objectFrameId.set(index,tile.id);
                index++;
            }
            const tileFrameMap = new KHashTable<string,AtlasFrameInfo>(strcmp,strhash);
            for(const frame of tileAtlas.frames){
                tileFrameMap.set(frame.frame.id,frame);
            }
            const objectFrameMap = new KHashTable<string,AtlasFrameInfo>(strcmp,strhash);
            for(const frame of objectAtlas.frames){
                objectFrameMap.set(frame.frame.id,frame);
            }
            //build tile set
            const tileTags:KXMLTag[] = [];
            index2tileFrameId.foreach((i,fid)=>{
                const frame = tileFrameMap.get(fid);
                if(frame!==undefined){
                    tileTags.push(buildTileTag(i,frame,tileAtlasPngFileName,tileAtlas.width,tileAtlas.height));
                }
            });
            index2objectFrameId.foreach((i,fid)=>{
                const frame = objectFrameMap.get(fid);
                if(frame!==undefined){
                    tileTags.push(buildTileTag(i,frame,objectAtlasPngFileName,objectAtlas.width,objectAtlas.height));
                }
            });
            const tsx:KXMLDocument = {
                header: {
                    version: "1.0",
                    encoding: "UTF-8"
                },
                dtd: {
                    rootTagName: "tileset"
                },
                rootTag: {
                    name: "tileset",
                    type: "2-side",
                    attributes: [
                        ["version","1.10"],
                        ["tiledversion","1.11.0"],
                        ["name",map.name],
                        ["tilewidth",map.tileWidth.toString()],
                        ["tileheight",map.tileHeight.toString()],
                        ["tilecount",index.toString()],
                    ],
                    children: tileTags
                }
            };
            fs.writeFileSync(path.join(outputDir,map.name+".tsx"),formatXml(tsx));
            //build tiled map
            const mapTags:KXMLTag[] = [{
                name: "tileset",
                type: "single",
                attributes: [
                    ["firstgid","1"],
                    ["source",map.name+".tsx"]
                ]
            }];
            let lid = 1;
            let oid = 1;
            for(const layer of map.layers){
                if(layer.type==="tile"){
                    //tile layer
                    let data = "";
                    for(let y=0; y<map.gridHeight; y++){
                        for(let x=0; x<map.gridWidth; x++){
                            const mid = layer.grids[y*map.gridWidth+x];
                            let i = 0;
                            if(mid!=0){
                                const ii = mapId2index.get(mid);
                                if(ii!==undefined){
                                    i = ii;
                                }
                            }
                            if(x<map.gridWidth-1){
                                data += i+",";
                            }
                            else{
                                if(y<map.gridHeight-1){
                                    data += i+",\n";
                                }
                                else{
                                    data += i+"\n";
                                }
                            }
                        }
                    }
                    mapTags.push({
                        name: "layer",
                        type: "2-side",
                        attributes: [
                            ["id",lid.toString()],
                            ["name",layer.name],
                            ["width",map.gridWidth.toString()],
                            ["height",map.gridHeight.toString()]
                        ],
                        children: [{
                            name: "data",
                            type: "2-side",
                            attributes: [["encoding","csv"]],
                            children: [data]
                        }]
                    });
                    lid++;
                }
                else{
                    //object layer
                    const objectTags:KXMLTag[] = [];
                    for(let y=0; y<map.gridHeight; y++){
                        for(let x=0; x<map.gridWidth; x++){
                            const mid = layer.grids[y*map.gridWidth+x];
                            let i = 0;
                            if(mid!=0){
                                const ii = mapId2index.get(mid);
                                if(ii!==undefined){
                                    i = ii;
                                }
                            }
                            if(i>0){
                                const fid = index2objectFrameId.get(i);
                                if(fid!==undefined){
                                    const frame = objectFrameMap.get(fid);
                                    if(frame!==undefined){
                                        objectTags.push({
                                            name: "object",
                                            type: "single",
                                            attributes: [
                                                ["id",oid.toString()],
                                                ["gid",i.toString()],
                                                ["x",(x*map.tileHeight+frame.frame.positionX).toString()],
                                                ["y",(y*map.tileHeight+frame.frame.positionY).toString()],
                                                ["width",frame.frame.width.toString()],
                                                ["height",frame.frame.height.toString()]
                                            ]
                                        });
                                        oid++;
                                    }
                                }
                            }
                        }
                    }
                    mapTags.push({
                        name: "objectgroup",
                        type: "2-side",
                        attributes: [
                            ["id",lid.toString()],
                            ["name",layer.name]
                        ],
                        children: objectTags
                    })
                    lid++;
                }
            }
            const tmx:KXMLDocument = {
                header: {
                    version: "1.0",
                    encoding: "UTF-8"
                },
                dtd: {
                    rootTagName: "map"
                },
                rootTag: {
                    name: "map",
                    attributes: [
                        ["version","1.10"],
                        ["tiledversion","1.11.0"],
                        ["orientation","isometric"],
                        ["renderorder","right-down"],
                        ["width",map.gridWidth.toString()],
                        ["height",map.gridHeight.toString()],
                        ["tilewidth",map.tileWidth.toString()],
                        ["tileheight",map.tileHeight.toString()],
                        ["infinite","0"]
                    ],
                    type: "2-side",
                    children: mapTags
                }
            };
            fs.writeFileSync(path.join(outputDir,map.name+".tmx"),formatXml(tmx,"  "),"utf8");
        }
    }
}
